#include "public.h"

int base64_decode(const string& src, string& dst)
{
	using namespace boost::archive::iterators;
	typedef transform_width<
			binary_from_base64< vector<char>::iterator >,
			8, 
			6
		> from_b64;
	vector<char> input(src.c_str(), src.c_str() + src.length());

	size_t pads = 0;
	while( input[ input.size() - 1 - pads ] == '=' )
	{
		input[ input.size() - 1 - pads ] = 'A';
		++pads;
	}
	string output;
	std::copy( from_b64( input.begin() ), 
			from_b64( input.end() ),
			back_inserter(output) );
	//remove the 0x00 add in the encoded data
	dst = output.substr(0, output.length() - pads );
	return 0;
}

size_t handle_write(void *ptr, size_t size, size_t nmemb, void *stream )
{
	DownloadInfo* pDi = (DownloadInfo*) stream;
	fseeko(pDi->output, pDi->bytesFrom + pDi->lastPieceRecved, SEEK_SET);

	int written = fwrite(ptr, size, nmemb, pDi->output );
	pDi->lastPieceRecved += written;
	pDi->bytesRecved += written;

	if( pDi->bytesRecved == pDi->bytesTotal )
	{
		logger->info("Download complete! thread index : %d", pDi->threadIndex);
		fclose(pDi->output);
	}else{
		// do nothing
	}
	return written;
}

int download_piece(DownloadInfoPtr pDi)
{
	int ret = 0;
	CURL* handle = curl_easy_init();
	pDi->handle = handle;
	//curl_easy_setopt(handle, CURLOPT_VERBOSE, 1);
	curl_easy_setopt(handle, CURLOPT_USERAGENT, "Mozilla/5.0");
	//curl_easy_setopt(handle, CURLOPT_PROXY, "http://122.72.0.6:80");
	curl_easy_setopt(handle, CURLOPT_URL, pDi->url.c_str() );
	curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, handle_write);
	curl_easy_setopt(handle, CURLOPT_WRITEDATA, pDi.get());
	while( pDi->bytesRecved < pDi->bytesTotal )
	{
		logger->debug("Index : %u, From : %llu, To : %llu, Recved : %llu",
			pDi->threadIndex, pDi->bytesFrom, pDi->bytesTo, pDi->bytesRecved);
		curl_easy_setopt(handle, CURLOPT_RANGE, (format("%llu-%llu")% pDi->bytesFrom % pDi->bytesTo).str().c_str() );
		curl_easy_perform(handle);
		long httpRetCode;
		CURLcode retCode = curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &httpRetCode);
		if( CURLE_OK != retCode )
		{
			logger->error("Cannot get return code , thread index : %u", pDi->threadIndex);
			ret = -1;
			break;
		}
		if( httpRetCode >= 300 )
		{
			logger->warn("Cannot fetch resource, http code : %ld", httpRetCode);
			ret = -1;
			break;
		}
		logger->debug("pDi->lastPieceRecved : %u", pDi->lastPieceRecved);
		long port;
		curl_easy_getinfo(pDi->handle, CURLINFO_LOCAL_PORT, &port);
		logger->debug("index : %u, port : %ld", pDi->threadIndex, port);
		pDi->bytesFrom += pDi->deltaOffset;
		if( pDi->bytesFrom > pDi->task->fileSize )
		{
			pDi->bytesFrom -= pDi->deltaOffset;
			pDi->bytesFrom = pDi->bytesTo + 1;
		}
		pDi->bytesTo = pDi->bytesFrom + pDi->pieceSize - 1;
		if( pDi->bytesTo > pDi->task->fileSize )
		{
			pDi->bytesTo = pDi->task->fileSize;
		}
		pDi->lastPieceRecved = 0;
	}
	return ret;
}

class Downloader{
	private:
		Config config;
		map<size_t, TaskPtr> tasks;
		size_t taskId;
	public:
		Downloader(const Config& config)
		:config(config),
		 taskId(0)
		{
		
		}
		int add_task(const TaskPtr& pTask)
		{
			pTask->id = taskId++;
			tasks[pTask->id] = pTask;
			int ret = this->fill_task_info( pTask );
			if( ret )
			{
				logger->warn("Cannot add task %s, url : %s ", pTask->filename.c_str(), pTask->url.c_str());
				return ret;
			}
			this->run_new_task( pTask );
			return 0;
		}

		int fill_task_info(const TaskPtr& pTask)
		{
			int ret = get_file_info(pTask->realUrl, pTask->fileSize, pTask->breakpointSupport);
			logger->info("file size : %.2lf bytes.", pTask->fileSize);
			if( pTask->fileSize < 0 )
			{
				logger->warn("unknown file size of %s.", pTask->filename.c_str());
				return -1;
			}
			if( ret == 0 )
			{
				return make_download_info(pTask);	
			}else{
				logger->error("get_file_info return %d!", ret);
				return -1;
			}
		}

		int get_file_info(const string& url, double& filesize, bool& breakpointSupport)
		{
			int ret = 0;
			CURL* handle = curl_easy_init();
			/*
			curl_slist* headers = NULL;
			curl_slist_append( headers, "user_agent: Mozilla/5.0");
			*/
			//curl_easy_setopt(handle, CURLOPT_VERBOSE, 1);
			curl_easy_setopt(handle, CURLOPT_USERAGENT, "Mozilla/5.0");
			curl_easy_setopt(handle, CURLOPT_PROXY, "http://122.72.0.6:80");
			curl_easy_setopt(handle, CURLOPT_URL, url.c_str());
			curl_easy_setopt(handle, CURLOPT_NOBODY, 1);
			
			CURLcode retCode = CURLE_OK;
			curl_easy_perform(handle);
			retCode = curl_easy_getinfo(handle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &filesize);
			if(retCode != CURLE_OK )
			{
				logger->warn("GET CURLINFO_CONTENT_LENGTH_DOWNLOAD ERROR ! %s", curl_easy_strerror(retCode));
				filesize = -1;
				ret = -1;
			}

			curl_easy_setopt(handle, CURLOPT_RANGE, "1-2");
			curl_easy_perform(handle);
			long httpRetCode;
			retCode = curl_easy_getinfo(handle, CURLINFO_RESPONSE_CODE, &httpRetCode);
			if( CURLE_OK != retCode )
			{
				logger->warn("GET CURLINFO_RESPONSE_CODE ERROR ! %s", curl_easy_strerror(retCode));
				breakpointSupport = false;
			}else{
				breakpointSupport = (206 == ret) ? true : false;
			}
			/*
			0 for ok
			-1 for fail to get filesize
			*/
			return ret;
		}

		void run_new_task(const TaskPtr& pTask)
		{
			vector<ThreadPtr> threads( pTask->threadCount );
			for(size_t i = 0; i != pTask->threadCount; ++i)
			{
				threads[i] = ThreadPtr( new thread( bind(&download_piece, pTask->dis[i])) );
			}
			BOOST_FOREACH(const ThreadPtr& pThread, threads)
			{
				pThread->join();
			}
			logger->notice("%s Download complete!", pTask->filename.c_str());
		}

		int make_download_info(const TaskPtr& pTask)
		{
				pTask->dis.reserve( pTask->threadCount );
				unsigned pieceSize, deltaOffset;
				uint64_t pieceCount, bytesLeft = pTask->fileSize;
				unsigned missionSize = pTask->fileSize / pTask->threadCount;

				if( missionSize <= 1*MB )
				{
					pieceSize = missionSize;
					deltaOffset = pieceSize * pTask->threadCount;
					pieceCount = 1;
				}else{
					pieceSize = 1*MB;
					deltaOffset = pieceSize * pTask->threadCount;
					pieceCount = pTask->fileSize / (1*MB*pTask->threadCount) + 1;
				}

				for(size_t pos = 0; pos != pTask->threadCount; ++pos)
				{
					DownloadInfoPtr info(new DownloadInfo);
					info->threadIndex = pos;
					info->task = pTask.get();	//raw point, do not release it!!!
					info->url = pTask->realUrl;
					info->pieceSize = pieceSize;
					info->deltaOffset = deltaOffset;
					info->bytesFrom = 0 + pos * pieceSize;
					info->bytesTo = info->bytesFrom + info->pieceSize - 1;
					const char* mode = "wb";
					info->output = fopen(pTask->filename.c_str(), mode);
					if( !info->output )
					{
						logger->error("Cannot open file : %s!", pTask->filename.c_str());
						goto err;
					}
					info->lastPieceRecved = info->bytesRecved = 0;
					if( pos != (pTask->threadCount - 1) )
					{
						info->bytesTotal = pieceSize * pieceCount;
						bytesLeft -= info->bytesTotal;
					}else{
						info->bytesTotal = bytesLeft;
					}
					logger->debug("index : %u, bytesTotal : %llu", info->threadIndex, info->bytesTotal);
					pTask->dis.push_back(info);
				}
			return 0;
		err:
			return -1;
		}
};

class Dispatcher{
	typedef boost::shared_ptr<Downloader> DownloaderPtr;
	enum LinkType{
		INVALID = -1,
		HTTP = 1,
		FTP = 2,
		THUNDER = 3,
		FLASHGET = 4,
		BITTORRENT = 5,
		MAGNET = 6,
	};
	typedef map<LinkType, DownloaderPtr> Services;
	
	Services services;
	public:
		Dispatcher()
		{
			init();
		}
		int init()
		{
			services[HTTP] = DownloaderPtr( new Downloader(Config("cooldown.cfg")) );
			return 0;
		}
		int add_task(const string& url, const string& filename)
		{
			TaskPtr pTask( new Task() );
			pTask->url = url;
			pTask->filename = filename;
			pTask->threadCount = boost::thread::hardware_concurrency();
			LinkType type = resolve_type(pTask);
			Services::iterator it = services.find( type );
			if( services.end() != it )
			{
				boost::thread t( bind(&Downloader::add_task, it->second, pTask) );
				return 0;
			}else{
				logger->warn("Service unavailable! type code : %d", type);
				return -1;
			}
		}

		LinkType resolve_type(TaskPtr& pTask)
		{
			size_t typePos = pTask->url.find("://");
			if( string::npos == typePos )
			{
				return INVALID;
			}
			
			string typeString( pTask->url.substr(0, typePos) );
			if( typeString == "http" )
			{
				pTask->realUrl = pTask->url;
				return HTTP;
			}else if (typeString == "thunder")
			{
				//thunder://AA + realUrl ++ ZZ
				// 3 for length of ://)
				// 4 for length of AA + ZZ
				const static size_t delimLength = 3;
				const static size_t fixsLength = 4;
				string encodedUrl = pTask->url.substr(0 + typeString.length() + delimLength );
				string decoded;
				if( base64_decode(encodedUrl, decoded) )
				{
					return INVALID;
				}
				size_t realLength = decoded.length() - fixsLength;
				pTask->realUrl = decoded.substr(2, realLength);
				logger->debug("decode url from \n%s \nto \n%s", pTask->url.c_str(), pTask->realUrl.c_str());
				
				return HTTP;
			}

			return INVALID;
		}
};		

/*
int handle_progress(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow)
{
	ProgressInfo *pt = (ProgressInfo*)clientp;
	double currentTime;
	curl_easy_getinfo(pt->handle, CURLINFO_TOTAL_TIME, &currentTime);
	double diffTime = currentTime - pt->lastTime;
	
	if( diffTime < 1 )
	{
		return 0;
	}

	pt->lastTime = currentTime;

	double last = pt->lastBytes;
	pt->lastBytes = dlnow;
	double diff = dlnow - last;
	cout << format("dlnow : %15.2lf KB, speed : %9.2lf KB/s") % (dlnow/1024) % (diff/(1024*diffTime)) << endl;

	return 0;
}
*/


int init_log()
{
	using namespace log4cpp;
	OstreamAppender* osAppender = new OstreamAppender("osAppender", &cout);
	FileAppender* fileAppender = new FileAppender("fileAppender", "cool-down.log");

	PatternLayout *layout=new PatternLayout();
        const char *format="[%d] %p %c %x - %m%n";
        layout->setConversionPattern(format);

	osAppender->setLayout( layout );
	fileAppender->setLayout( layout );
	Category& root = Category::getRoot();
	logger = &root;
	root.addAppender(osAppender);
	root.addAppender(fileAppender);
	root.setPriority(Priority::DEBUG);
	
	return 0;
}

int init_curl()
{
	return curl_global_init(CURL_GLOBAL_ALL);
}


int main(int argc, char* argv[])
{
	if( 3 == argc )
	{
		int ret;
		if( (ret = init_log()) != 0 )
		{
			DEBUG_OUTPUT("init_log error!");
			goto err;
		}
		if( (ret = init_curl()) != CURLE_OK )
		{
			logger->crit("init_curl error! return code : %d", ret);
			goto err;
		}
		Dispatcher d;
		d.add_task( argv[1], argv[2] );
		string something;
		while( cin >> something )
		{
			cout << "wait for output!" << endl;
		}
		return EXIT_SUCCESS;
	}else if( 2 == argc )
	{
		string output;
		base64_decode(argv[1], output);
		cout << output << endl;
	}
err:
	return EXIT_FAILURE;
}
